// Copyright (c) 2012 Daniel Kang. All rights reserved.
// Copyright (c) 2013 Alex Caudill. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#pragma once

#include "base.hpp"
#include "handle.hpp"
#include "net.hpp"
#include "text.hpp"
#include "callback.hpp"

#include <sstream>
#include "http_parser.h"

namespace market { 
    namespace node {
	    namespace http {

		    class url_parse_exception : public market::node::exception {
		        public:
			        url_parse_exception(
                        const std::string& message="Failed to parse URL."
                    ) : market::node::exception(message)
			        {}
		    };

    		class response_exception : public market::node::exception {
		        public:
			        response_exception(
                        const std::string& message="HTTP response error."
                    ) : market::node::exception(message)
			        {}
		    };

		    class url_obj {
			    friend class client_context;

		        public:
			        url_obj() : handle_(), buf_()
		            {
				        //printf("url_obj() %x\n", this);
			        }

			        url_obj(
                        const url_obj& c
                    ) : handle_(c.handle_),
                        buf_(c.buf_)
			        {
				        //printf("url_obj(const url_obj&) %x\n", this);
			        }

			        url_obj&
                    operator =
                    (const url_obj& c)
			        {
				        //printf("url_obj::operator =(const url_obj&) %x\n", this);
				        handle_ = c.handle_;
				        buf_ = c.buf_;
				        return *this;
			        }

			        ~url_obj()
			        {
				        //printf("~url_obj() %x\n", this);
			        }

		        public:
			        std::string
                    schema() const
			        {
				        if(has_schema()) { 
                            return buf_.substr(
                                handle_.field_data[UF_SCHEMA].off,
                                handle_.field_data[UF_SCHEMA].len
                            );
                        }
				        return "HTTP";
			        }

			        std::string
                    host() const
                    {
				        // TODO: if not specified, use host name
				        if(has_schema()) {
                            return buf_.substr(
                                handle_.field_data[UF_HOST].off,
                                handle_.field_data[UF_HOST].len
                            );
                        } else { 
				            return std::string("localhost");
                        }
			        }

			        int
                    port() const
			        {
				        if(has_path()) { 
                            return static_cast<int>(handle_.port);
                        } else { 
				            return (schema() == "HTTP" ? 80 : 443);
                        }
			        }

			        std::string
                    path() const
			        {
				        if(has_path()) {
                            return buf_.substr(
                                handle_.field_data[UF_PATH].off,
                                handle_.field_data[UF_PATH].len
                            );
                        } else { 
				            return std::string("/");
			            }
                    }

			        std::string
                    query() const
			        {
				        if(has_query()) {
                            return buf_.substr(
                                handle_.field_data[UF_QUERY].off,
                                handle_.field_data[UF_QUERY].len
                            );
                        } else { 
				            return std::string();
			            }
                    }

        			std::string
                    fragment() const
			        {
				        if(has_query()) {
                            return buf_.substr(
                                handle_.field_data[UF_FRAGMENT].off,
                                handle_.field_data[UF_FRAGMENT].len
                            );
                        } else { 
				            return std::string();
			            }
                    }

		        private:
			        void from_buf(
                        const char* buf,
                        std::size_t len,
                        bool is_connect=false
                    )
			        {
				        // TODO: validate input parameters

				        buf_ = std::string(buf, len);
				        if(http_parser_parse_url(buf, len, is_connect, &handle_) != 0) {
					        // failed for some reason
					        // TODO: let the caller know the error code (or error message)
					        throw url_parse_exception();
				        }
			        }

			        bool
                    has_schema() const
                    {
                        return handle_.field_set & (1<<UF_SCHEMA);
                    }

        			bool
                    has_host() const
                    {
                        return handle_.field_set & (1<<UF_HOST);
                    }

        			bool
                    has_port() const
                    {
                        return handle_.field_set & (1<<UF_PORT);
                    }

        			bool
                    has_path() const
                    {
                        return handle_.field_set & (1<<UF_PATH);
                    }

        			bool
                    has_query() const
                    {
                        return handle_.field_set & (1<<UF_QUERY);
                    }

		        	bool
                    has_fragment() const
                    {
                        return handle_.field_set & (1<<UF_FRAGMENT);
                    }

		        private:
			        http_parser_url handle_;
			        std::string buf_;
		    };

		    class client_context;
		    typedef std::shared_ptr<client_context> http_client_ptr;

		    class response {
			    friend class client_context;

		        private:
			        response(
                        client_context* client,
                        market::node::net::tcp* socket
                    ) : client_(client),
                        socket_(socket),
                        headers_(),
                        status_(200)
			        {
				        headers_["Content-Type"] = "text/html";
			        }

			        ~response(){}

		        public:
			        bool
                    end(
                        const std::string& body
                    )
			        {
				        // Content-Length
				        if(headers_.find("Content-Length") == headers_.end()) {
					        std::stringstream ss;
					        ss << body.length();
					        headers_["Content-Length"] = ss.str();
				        }

				        std::stringstream response_text;
				        response_text << "HTTP/1.1 ";
				        response_text << status_ << " " << get_status_text(status_) << "\r\n";
				        for(auto h : headers_) {
					        response_text << h.first << ": " << h.second << "\r\n";
				        }
				        response_text << "\r\n";
				        response_text << body;

				        auto str = response_text.str();
				        return socket_->write(
                            str.c_str(),
                            static_cast<int>(str.length()),
                            [=](error e) {
				                if(e) {
				                    // TODO: handle error
				                }
					            // clean up
					            client_.reset();
				            }
                        );
			        }

                    void
                    set_status(
                        int status_code
                    )
                    {
				        status_ = status_code;
                    }

                    void
                    set_header(
                        const std::string& key,
                        const std::string& value
                    )
                    {
				        headers_[key] = value;
			        }

			        static std::string
                    get_status_text(
                        int status
                    )
			        {
				        switch(status) {
                            case 100: return "Continue";
                            case 101: return "Switching Protocols";
                            case 200: return "OK";
                            case 201: return "Created";
                            case 202: return "Accepted";
                            case 203: return "Non-Authoritative Information";
                            case 204: return "No Content";
                            case 205: return "Reset Content";
                            case 206: return "Partial Content";
                            case 300: return "Multiple Choices";
                            case 301: return "Moved Permanently";
                            case 302: return "Found";
                            case 303: return "See Other";
                            case 304: return "Not Modified";
                            case 305: return "Use Proxy";
                            //case 306: return "(reserved)";
                            case 307: return "Temporary Redirect";
                            case 400: return "Bad Request";
                            case 401: return "Unauthorized";
                            case 402: return "Payment Required";
                            case 403: return "Forbidden";
                            case 404: return "Not Found";
                            case 405: return "Method Not Allowed";
                            case 406: return "Not Acceptable";
                            case 407: return "Proxy Authentication Required";
                            case 408: return "Request Timeout";
                            case 409: return "Conflict";
                            case 410: return "Gone";
                            case 411: return "Length Required";
                            case 412: return "Precondition Failed";
                            case 413: return "Request Entity Too Large";
                            case 414: return "Request-URI Too Long";
                            case 415: return "Unsupported Media Type";
                            case 416: return "Requested Range Not Satisfiable";
                            case 417: return "Expectation Failed";
                            case 500: return "Internal Server Error";
                            case 501: return "Not Implemented";
                            case 502: return "Bad Gateway";
                            case 503: return "Service Unavailable";
                            case 504: return "Gateway Timeout";
                            case 505: return "HTTP Version Not Supported";
                            default: throw response_exception("Not supported status code.");
                        }
			        }

		            private:
                        http_client_ptr client_;
                        market::node::net::tcp* socket_;
                        std::map<std::string, std::string, market::node::text::ci_less> headers_;
                        int status_;
                };

                class request {
                    friend class client_context;

		            private:
                        request() : url_(), headers_() {}

			            ~request()
                        {
				            //printf("~request() %x\n", this);
                        }

            		public:
			            const url_obj& url() const { return url_; }

			            const std::string&
                        get_header(
                            const std::string& key
                        ) const
			            {
				            auto it = headers_.find(key);
				            if(it != headers_.end()) { 
                                return it->second;
                            }
				            return default_value;
			            }

			            bool
                        get_header(
                            const std::string& key,
                            std::string& value
                        ) const
                        {
                            auto it = headers_.find(key);
                            if(it != headers_.end()) {
                                value = it->second;
                                return true;
                            }
                            return false;
			            }

                private:
                    const std::string& default_value=std::string();
			        url_obj url_;
			        std::map<std::string, std::string, market::node::text::ci_less> headers_;
		    };

            class client_context {
			    friend class http;

                private:
			        client_context(
                        market::node::net::tcp* server
                    ) : parser_(),
                        parser_settings_(),
                        was_header_value_(true),
                        last_header_field_(),
                        last_header_value_(),
                        socket_(nullptr),
                        request_(nullptr),
                        response_(nullptr),
                        callback_lut_(new callbacks(1))
                    {
                        //printf("request() %x callback_=%x\n", this, callback_);
                        assert(server);

                        // TODO: check error
                        socket_ = new market::node::net::tcp;
                        server->accept(socket_);
                    }

                public:
                    ~client_context()
                    {
                        if(request_) {
                            delete request_;
                            request_ = nullptr;
                        }

                        if(response_) {
                            delete response_;
                            response_ = nullptr;
                        }

                        if(callback_lut_) {
                            delete callback_lut_;
                            callback_lut_ = nullptr;
                        }

                        if(socket_) {
                            // TODO: maybe close() does not affect socket_ pointer itself. So, delete socket_ does not have to be inside the callback.
                            socket_->close([=](){
                                delete socket_;
                                socket_ = nullptr;
                            });
                        }
                    }

                private:
                    bool parse(
                        std::function<void(request&, response&)> callback
                    )
                    {
                        request_ = new request;
                        response_ = new response(this, socket_);

                        http_parser_init(&parser_, HTTP_REQUEST);
                        parser_.data = this;

                        // store callback object
                        callbacks::store(callback_lut_, 0, callback);

                        parser_settings_.on_url = [](http_parser* parser, const char *at, size_t len) {
                            auto client = reinterpret_cast<client_context*>(parser->data);

                            //  TODO: from_buf() can throw an exception: check
                            client->request_->url_.from_buf(at, len);
                            return 0;
                        };

                        parser_settings_.on_header_field = [](http_parser* parser, const char* at, size_t len) {
                            auto client = reinterpret_cast<client_context*>(parser->data);

                            if(client->was_header_value_) {
                                // new field started
                                if(!client->last_header_field_.empty()) {
                                    // add new entry
                                    client->request_->headers_[client->last_header_field_] = client->last_header_value_;
                                    client->last_header_value_.clear();
                                }

                                client->last_header_field_ = std::string(at, len);
                                client->was_header_value_ = false;
                            } else {
                                // appending
                                client->last_header_field_ += std::string(at, len);
                            }
                            return 0;
                        };

                        parser_settings_.on_header_value = [](http_parser* parser, const char* at, size_t len) {
                            auto client = reinterpret_cast<client_context*>(parser->data);

                            if(!client->was_header_value_) {
                                client->last_header_value_ = std::string(at, len);
                                client->was_header_value_ = true;
                            } else {
                                // appending
                                client->last_header_value_ += std::string(at, len);
                            }
                            return 0;
                        };

                        parser_settings_.on_headers_complete = [](http_parser* parser) {
                            auto client = reinterpret_cast<client_context*>(parser->data);

                            // add last entry if any
                            if(!client->last_header_field_.empty()) {
                                // add new entry
                                client->request_->headers_[client->last_header_field_] = client->last_header_value_;
                            }

                            //return 0;
                            return 1; // do not parse body
                        };

                        parser_settings_.on_message_complete = [](http_parser* parser) {
                            auto client = reinterpret_cast<client_context*>(parser->data);
                            // invoke stored callback object
                            callbacks::invoke<decltype(callback)>(
                                client->callback_lut_,
                                0,
                                *client->request_,
                                *client->response_
                            );
                            return 0;
                        };

                        socket_->read_start([=](const char* buf, int len){
                            http_parser_execute(&parser_, &parser_settings_, buf, len);
                        });

                        return true;
                    }

                private:
                    http_parser parser_;
                    http_parser_settings parser_settings_;
                    bool was_header_value_;
                    std::string last_header_field_;
                    std::string last_header_value_;

                    market::node::net::tcp* socket_;
                    request* request_;
                    response* response_;

                    callbacks* callback_lut_;
            };

            class http {
                public:
                    http() : socket_(new market::node::net::tcp) {}

                    virtual ~http() {
                        if(socket_) {
                            socket_->close([](){});
                        }
                    }

                public:
                    static std::shared_ptr<http>
                    create_server(
                        const std::string& ip,
                        int port,
                        std::function<void(request&, response&)> callback
                    )
                    {
                        auto server = std::shared_ptr<http>(new http);
                        if(server->listen(ip, port, callback)) {
                            return server;
                        }
                        return nullptr;
                    }

                    bool listen(
                        const std::string& ip,
                        int port,
                        std::function<void(request&, response&)> callback
                    )
                    {
                        if(!socket_->bind(ip, port)) {
                            return false;
                        }

                        if(!socket_->listen([=](error e) {
                            if(e) {
                                // TODO: handle client connection error
                            } else {
                                auto client = new client_context(socket_.get());
                                client->parse(callback);
                            }
                        }))  return false;

				        return true;
			        }

                    void
                    close()
                    {
                        if(socket_.get()) {
                            socket_->close([]{});
                            socket_.reset();
                        }
                    }

                private:
                    std::shared_ptr<market::node::net::tcp> socket_;
            };

            typedef http_method method;
            typedef http_parser_url_fields url_fields;
            typedef http_errno error;

            inline const char*
            get_error_name(
                error err
            )
            {
                return http_errno_name(err);
            }

            inline const char*
            get_error_description(
                error err
            )
            {
                return http_errno_description(err);
            }

            inline const char*
            get_method_name(
                method m
            )
            {
                return http_method_str(m);
            }

        } // namespace http
	} // namespace node
} // namespace market
